1"use client";
2
3
26import { useEffect, useState } from "react";
27import { useParams, useRouter, useSearchParams } from "next/navigation";
28import Link from "next/link";
29import { useQuery } from "@tanstack/react-query";
30import { ShieldCheck } from "lucide-react";
31import { useAuth } from "@/hooks/use-auth";
32import { orgAdminApi } from "@/lib/org-admin";
33import { isApiError } from "@/lib/api";
34import { cn } from "@/lib/utils";
35import { SsoTabs } from "@/components/admin/SsoTabs";
36import { ScimCard } from "@/components/admin/ScimCard";
37import { AdminAuditTab } from "@/components/admin/org-admin/AuditTab";
38import { AdminMembersTab } from "@/components/admin/org-admin/MembersTab";
39import { AdminAnalyticsTab } from "@/components/admin/org-admin/AnalyticsTab";
40import { AdminOverviewTab } from "@/components/admin/org-admin/OverviewTab";
41import { AdminPoliciesTab } from "@/components/admin/org-admin/PoliciesTab";
42import { AdminRetentionTab } from "@/components/admin/org-admin/RetentionTab";
43import { AdminApiKeysTab } from "@/components/admin/org-admin/ApiKeysTab";
44import { AdminWebhooksTab } from "@/components/admin/org-admin/WebhooksTab";
45import { AdminJobsTab } from "@/components/admin/org-admin/JobsTab";
46import { AdminSearchHealthTab } from "@/components/admin/org-admin/SearchHealthTab";
47import { AdminNotificationsTab } from "@/components/admin/org-admin/NotificationsTab";
48import { WorkspacePicker } from "@/components/admin/org-admin/WorkspacePicker";
49
50const TABS = [
51 { id: "overview", label: "Overview" },
52 { id: "analytics", label: "Analytics" },
53 { id: "members", label: "Members" },
54 { id: "integrations", label: "Integrations" },
55 { id: "policies", label: "Policies" },
56 { id: "retention", label: "Retention" },
57 { id: "api-keys", label: "API keys" },
58 { id: "webhooks", label: "Webhooks" },
59 { id: "audit", label: "Audit" },
60 { id: "jobs", label: "Jobs" },
61 { id: "search-health", label: "Search health" },
62 { id: "notifications-health", label: "Notifications" },
63] as const;
64type TabId = (typeof TABS)[number]["id"];
65
66function isTabId(v: string | null): v is TabId {
67 return TABS.some((t) => t.id === v);
68}
69
70export default function OrgAdminConsolePage() {
71 const { orgId } = useParams<{ orgId: string }>();
72 const router = useRouter();
73 const searchParams = useSearchParams();
74 const { user, initializing } = useAuth();
75 const [tab, setTab] = useState<TabId>("overview");
76
77 useEffect(() => {
78 if (!initializing && !user) {
79 const next = window.location.pathname + window.location.search;
80 router.replace(`/login?next=${encodeURIComponent(next)}`);
81 }
82 }, [initializing, user, router]);
83
84
85 useEffect(() => {
86 const t = searchParams.get("tab");
87 if (isTabId(t)) setTab(t);
88 }, [searchParams]);
89
90 const selectTab = (id: TabId) => {
91 setTab(id);
92 router.replace(`/org/${orgId}/admin?tab=${id}`, { scroll: false });
93 };
94
95
96
97
98 const [wsId, setWsId] = useState<string | null>(null);
99
100 const orgWorkspaces = useQuery({
101 queryKey: ["orgs", orgId, "admin", "workspaces"],
102 queryFn: () => orgAdminApi.workspaces(String(orgId)),
103 enabled: Boolean(orgId),
104 retry: false,
105 });
106
107
108 useEffect(() => {
109 const first = orgWorkspaces.data?.data[0]?.id;
110 if (first && wsId === null) setWsId(first);
111 }, [orgWorkspaces.data, wsId]);
112
113
114 const forbidden = isApiError(orgWorkspaces.error) && orgWorkspaces.error.status === 403;
115
116 if (initializing || !user) {
117 return (
118 <div className="flex min-h-screen items-center justify-center bg-background">
119 <div className="flex items-center gap-3 text-sm text-muted-foreground" role="status">
120 <span className="size-4 animate-spin rounded-full border-2 border-muted-foreground/30 border-t-foreground" />
121 Loading…
122 </div>
123 </div>
124 );
125 }
126
127 const workspaces = orgWorkspaces.data?.data ?? [];
128
129 return (
130 <div className="min-h-screen bg-background">
131 <header className="sticky top-0 z-10 border-b border-border bg-background/95 backdrop-blur">
132 <div className="mx-auto max-w-6xl px-4 pt-3">
133 <div className="flex items-center gap-2">
134 <Link href="/w/default" className="text-sm text-muted-foreground hover:text-foreground">
135 ← Workspaces
136 </Link>
137 <span className="text-muted-foreground">/</span>
138 <span className="inline-flex items-center gap-1.5 text-sm font-semibold">
139 <ShieldCheck className="size-4" /> Organization admin
140 </span>
141 </div>
142 <nav className="-mb-px mt-2 flex gap-1 overflow-x-auto" aria-label="Org admin tabs">
143 {TABS.map((t) => (
144 <button
145 key={t.id}
146 type="button"
147 onClick={() => selectTab(t.id)}
148 aria-current={tab === t.id ? "page" : undefined}
149 className={cn(
150 "whitespace-nowrap rounded-t-md border-b-2 px-3 py-2 text-sm",
151 tab === t.id
152 ? "border-foreground font-medium text-foreground"
153 : "border-transparent text-muted-foreground hover:text-foreground"
154 )}
155 >
156 {t.label}
157 </button>
158 ))}
159 </nav>
160 </div>
161 </header>
162
163 <main className="mx-auto max-w-6xl space-y-6 px-4 py-6">
164 {forbidden ? (
165 <div
166 data-testid="org-admin-forbidden"
167 className="rounded-lg border border-destructive/40 bg-destructive/5 p-4 text-sm text-destructive"
168 >
169 You need <strong>org-admin</strong> rights to open this console. Ask the organization owner to grant you
170 the ADMIN role.
171 </div>
172 ) : !orgId ? (
173 <p className="text-sm text-muted-foreground">Missing org id.</p>
174 ) : (
175 <>
176 {}
178 {(tab === "api-keys" || tab === "webhooks") && (
179 <WorkspacePicker orgId={String(orgId)} workspaces={workspaces} value={wsId} onChange={setWsId} />
180 )}
181 {tab === "overview" && <AdminOverviewTab orgId={String(orgId)} />}
182 {tab === "analytics" && <AdminAnalyticsTab orgId={String(orgId)} />}
183 {tab === "members" && <AdminMembersTab orgId={String(orgId)} />}
184 {tab === "integrations" && (
185 <div className="space-y-8">
186 <SsoTabs orgId={String(orgId)} />
187 <ScimCard orgId={String(orgId)} />
188 </div>
189 )}
190 {tab === "policies" && <AdminPoliciesTab orgId={String(orgId)} />}
191 {tab === "retention" && <AdminRetentionTab orgId={String(orgId)} />}
192 {tab === "api-keys" && <AdminApiKeysTab workspaceId={wsId} />}
193 {tab === "webhooks" && <AdminWebhooksTab workspaceId={wsId} />}
194 {tab === "audit" && <AdminAuditTab orgId={String(orgId)} />}
195 {tab === "jobs" && <AdminJobsTab orgId={String(orgId)} />}
196 {tab === "search-health" && <AdminSearchHealthTab orgId={String(orgId)} />}
197 {tab === "notifications-health" && <AdminNotificationsTab orgId={String(orgId)} />}
198 </>
199 )}
200 </main>
201 </div>
202 );
203}
204